﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using RayDen.Library.Components;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Data;
using RayDen.Library.Data.Imaging;
using RayDen.Library.Entity;
using RayDen.Library.Entity.Frames;
using RayDen.Library.Entity.Interface;
using RayDen.Library.Entity.Scene;
using RayDen.Library.Entity.SceneGraph;


using RayDen.SmallPT.Light;

namespace RayDen.SmallPT
{
    public class SceneBuilder : ISceneBuilder
    {
        public Dictionary<string, ILight> Lights;
        public List<MaterialInfo> MaterialInfos;
        public List<CameraInfo> Cameras;

        public SceneGeometryInfo SceneGeo;

        public InfiniteLight EnvironmentMap;
        public TextureAtlas Textures;
        public Dictionary<string, string[]> texturesMap;
        public BaseRenderer Renderer;

        protected List<INodeVisitor> visitors;

        public SceneGraphService SGService { get; set; }

        public void Setup(BaseRenderer renderer)
        {
            Renderer = renderer;
            Lights = new Dictionary<string, ILight>();
            MaterialInfos = new List<MaterialInfo>();
            Cameras = new List<CameraInfo>();

            visitors = new List<INodeVisitor>
                {
                    CreateVisitor<MaterialsBuilder>(),
                    CreateVisitor<LightBuilder>(),
                    CreateVisitor<CameraBuilder>(),
                    CreateVisitor<GeoDataBuilder>()//MeshBuilder
                };
        }

        public void OpenFrame(IRayDenLibraryFrame frame)
        {
            if (SGService == null)
                SGService = new SceneGraphService();
            SGService.OpenFrame((FrameDescription)frame);
            var sceneGraph = SGService.SceneGraph;
            visitors.ForEach(item => ((DependentComponentBase)item).Populate(sceneGraph, Renderer));
            foreach (var nodeVisitor in visitors)
            {
                sceneGraph.Visit(nodeVisitor);
            }

            //LoadTextures();

            Renderer.MaterialProvider = new DefaultMaterialContainer(new MaterialInfo(), this.MaterialInfos);
            Renderer.lights = this.Lights.Values.ToList();
            Renderer.scene = this.SceneGeo;
            
            //scene.texturesMap = texturesMap;
            //scene.Textures = this.Textures;

            Renderer.EnvironmentMap = EnvironmentMap;
            var width = frame.Get<int>("ImageWidth");
            var height = frame.Get<int>("ImageHeight");
            Renderer.camera = new BasicPerspectiveCamera((Point)Cameras.First().Position, Cameras.First().Direction,Cameras.First().Up, width, height);
            //new BasicPerspectiveCamera((Point) Cameras.First().Position, Cameras.First().Direction, Cameras.First().Up, width, height);
            this.Renderer.areaLights = new List<BaseRenderer.AreaLightInfo>();
            this.Renderer.frame = (FrameDescription)frame;
            this.Renderer.width = frame.Get<int>("ImageWidth");
            this.Renderer.height = frame.Get<int>("ImageHeight");
            //Directory.SetCurrentDirectory(frame.Dir);
            this.Renderer.meshes = new List<TriangleMesh>();
            foreach (var geometryInfo in SceneGeo.Geometry)
            {
                var mesh = new TriangleMesh();
                mesh.Init(geometryInfo, SceneGeo);
                this.Renderer.meshes.Add(mesh);
            }
            var meshLights = this.Lights.Select(item => item.Value).Where(item => item is MeshLight).Cast<MeshLight>();
            foreach (var meshLight in meshLights)
            {
                meshLight.mesh =
                    Renderer.meshes.SingleOrDefault(
                        item => item.MeshName.Equals(meshLight.MeshName, StringComparison.InvariantCultureIgnoreCase));
            }

            this.Renderer.areaLights = meshLights.Select(item=>new BaseRenderer.AreaLightInfo(){ Gain = item.gain, GeoName = item.MeshName}).ToList();
            if (this.Renderer.areaLights.Any())
            {
                foreach (var areaLightInfo in this.Renderer.areaLights)
                {
                    BaseRenderer.AreaLightInfo info = areaLightInfo;
                    var geo = Renderer.meshes.FirstOrDefault(item => item.MeshName.Equals(info.GeoName));
                    Renderer.lights.Add(new MeshLight(geo, areaLightInfo.Gain));
                    /*
                    for (int index = 0; index < geo.triangles.Count; index++)
                    {
                        var triangle = geo.triangles[index];
                        var tLt = new RayDen.SmallPT.Light.TriangleLight(scene, ref triangle, index, areaLightInfo.Gain);
                        lights.Add(tLt);
                        triLights.Add(tLt);
                    }
                     */
                }
            }

            this.Renderer.manager = new SceneManager(Renderer.meshes.ToArray(), SceneGeo, MaterialInfos.ToArray());
            this.Renderer.matLib = new BsdfLibrary();
            this.Renderer.matLib.Populate(MaterialInfos.ToArray());
        }



        private TVisitor CreateVisitor<TVisitor>(params object[] deps)
            where TVisitor : DependentComponentBase, INodeVisitor, ISceneBuilderNodeHandler, new()
        {
            var vis = new TVisitor();
            vis.Populate(deps, this);
            vis.Owner = this;
            return vis;
        }

        private void LoadTextures()
        {
            Console.WriteLine("Start loading textures");
            texturesMap = new Dictionary<string, string[]>();
            Textures = new TextureAtlas();

            foreach (var materialInfo in this.MaterialInfos)
            {
                var textures = new List<string>();

                if (materialInfo.Diffuse != null)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.Diffuse, textures);
                }

                if (materialInfo.BumpMap != null)
                {
                    ProcessMaterialTexture((ImageTextureInfo)materialInfo.BumpMap, textures);
                }

                if (textures.Any())
                    texturesMap.Add(materialInfo.Name.ToLower(), textures.ToArray());
            }
            Console.WriteLine("Complete loading textures");
        }

        private void ProcessMaterialTexture(TextureInfo tex, List<string> textures)
        {
            try
            {
                var diff = tex as ImageTextureInfo;
                if (diff != null)
                {
                    var texPath = this.FindTexture(diff);
                    if (texPath != null)
                    {
                        this.Textures.AddTexture(new ImageTextureInfo()
                            {
                                FilePath = texPath,
                                Name = Path.GetFileName(texPath)
                            });
                        textures.Add(Path.GetFileName(texPath));
                    }
                }
                var nz = tex as SolidTextureInfo;
                if (nz != null)
                {
                    this.Textures.AddTexture(nz);
                    textures.Add(nz.Name);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine("Couldnt load texture {0}", tex.Name);
                Console.WriteLine(ex.Message);
                Console.WriteLine(ex.StackTrace);
            }
        }

        public string FindTexture(ImageTextureInfo ii)
        {
            string filePath = ii.FilePath;
            if (File.Exists(filePath))
                return filePath;
            if (File.Exists(ii.Name))
            {
                return Directory.GetCurrentDirectory() + @"\" + ii.Name;
            }
            return null;
        }

        public void Add<TSceneElement>(TSceneElement item)
            where TSceneElement:class 
        {
            if (item is InfiniteLight)
            {
                this.EnvironmentMap = item as InfiniteLight;
            }

            if (item is ILight)
            {
                this.Lights.Add((item as ILight).Name, item as ILight);
            }  else if (item is CameraInfo)
            {
                this.Cameras.Add(item as CameraInfo);
            } else if (item is SceneGeometryInfo)
            {
                this.SceneGeo = item as SceneGeometryInfo;
            } else if (item is MaterialInfo)
            {
                this.MaterialInfos.Add(item as MaterialInfo);
            }
        }
    }

    public abstract class SceneBuilderComponentBase<TElement> : SceneBuilderComponentBase<TElement, SceneBuilder>
     where TElement : SceneGraphElement
    {

    }

    internal class MaterialsBuilder : SceneBuilderComponentBase<SceneMaterialDataNode>
    {
        protected override void VisitInternal(SceneMaterialDataNode node)
        {
            var mi = (MaterialInfo)node.GetDataItem();
            this.Owner.Add(mi);
            //tex
        }

    }

    internal class MeshBuilder : SceneBuilderComponentBase<GeometryInstanceNode>
    {
        protected override void VisitInternal(GeometryInstanceNode node)
        {
            var geoData = (GeometryInfo)SceneGraph.ResolveDataNode<GeometryElementNode>(node).GetDataItem();
            //Transform
        }
    }

    internal class GeoDataBuilder : SceneBuilderComponentBase<GeometryGroupNode>
    {
        protected override void VisitInternal(GeometryGroupNode node)
        {
            var sceneData = (SceneGeometryInfo)SceneGraph.ResolveDataNode<SceneGeometryDataNode>(node).GetDataItem();
            this.Owner.Add(sceneData);
        }
    }

    internal class LightBuilder : SceneBuilderComponentBase<LightsourceNode>
    {

        protected override void VisitInternal(LightsourceNode node)
        {
            var li = (LightsourceInfo)node.GetDataItem();
            ILight light = null;
            switch (li.Type)
            {
                case LightSourceTypes.Area:
                    var meshLight = new MeshLight()
                    {
                        gain = new RgbSpectrum(li.Gain),
                        MeshName = li.MeshName,
                        spectra = SPD_Data.FromSampled(SPD_Data.A, SpectrumType.Illuminant),
                        LightName = li.MeshName
                    };
                    light = meshLight;
                    break;
                case LightSourceTypes.EnvironmentMap:
                    if ((this.Owner as SceneBuilder).EnvironmentMap == null)
                    {
                        (this.Owner as SceneBuilder).EnvironmentMap = new InfiniteLight(
                            Path.GetFileName(li.ImageName).NameContains("hdr", "exr")
                                ? ImageFactory.FreeImageLoadExrBitmap(li.ImageName)
                                : ImageFactory.FreeImageLoadBitmap(li.ImageName));
                        light = (this.Owner as SceneBuilder).EnvironmentMap;
                    }
                    break;
                case LightSourceTypes.Point:
                    light =
                        //new PointLight()
                        new SphereAreaLight()
                        {
                            Position = (Point)li.Position,
                            Power = new RgbSpectrum(li.Spectra),
                            Spectra = SPD_Data.FromSampled(SPD_Data.D5000, SpectrumType.Illuminant)
                        };
                    break;
            }
            if (light != null)
            {
                this.Owner.Add(light);
            }
        }
    }

    public class CameraBuilder : SceneBuilderComponentBase<CameraNode>
    {
        protected override void VisitInternal(CameraNode node)
        {
            this.Owner.Add((CameraInfo)node.GetDataItem());
        }
    }
}